Conversation
|
""" Walkthrough이번 변경에서는 Changes
Sequence Diagram(s)sequenceDiagram
participant App
participant TokenPreferencesDataSource
participant DefaultTokenPreferencesDataSource
participant DataStore
participant CryptoManager
App->>TokenPreferencesDataSource: setAccessToken(token)
TokenPreferencesDataSource->>DefaultTokenPreferencesDataSource: setAccessToken(token)
DefaultTokenPreferencesDataSource->>CryptoManager: encrypt(token)
CryptoManager-->>DefaultTokenPreferencesDataSource: encryptedToken
DefaultTokenPreferencesDataSource->>DataStore: store(encryptedToken)
App->>TokenPreferencesDataSource: accessToken.collect()
TokenPreferencesDataSource->>DefaultTokenPreferencesDataSource: accessToken Flow
DefaultTokenPreferencesDataSource->>DataStore: read(encryptedToken)
DataStore-->>DefaultTokenPreferencesDataSource: encryptedToken
DefaultTokenPreferencesDataSource->>CryptoManager: decrypt(encryptedToken)
CryptoManager-->>DefaultTokenPreferencesDataSource: token
DefaultTokenPreferencesDataSource-->>App: token (as Flow)
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes(해당 변경사항에는 linked issue의 목적과 무관한 코드 변경이 발견되지 않았습니다.) Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/TokenPreferencesDataSource.kt (1)
5-11: 인터페이스 설계가 깔끔하고 반응형 프로그래밍 패턴을 잘 따르고 있습니다.토큰 관리를 위한 명확한 계약을 정의했으며, Flow를 통한 반응형 접근과 suspend 함수를 통한 비동기 작업을 적절히 분리했습니다.
다음과 같은 KDoc 문서화를 추가하는 것을 고려해보세요:
+/** + * 토큰 관리를 위한 데이터 소스 인터페이스 + * 암호화된 토큰 저장 및 조회 기능을 제공합니다. + */ interface TokenPreferencesDataSource { + /** 암호화된 액세스 토큰을 복호화하여 반환하는 Flow */ val accessToken: Flow<String> + /** 암호화된 리프레시 토큰을 복호화하여 반환하는 Flow */ val refreshToken: Flow<String> + /** 액세스 토큰을 암호화하여 저장 */ suspend fun setAccessToken(accessToken: String) + /** 리프레시 토큰을 암호화하여 저장 */ suspend fun setRefreshToken(refreshToken: String) + /** 저장된 모든 토큰을 삭제 */ suspend fun clearTokens() }core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/security/CryptoManager.kt (2)
49-64: 암호화/복호화 메서드에 예외 처리를 추가하세요.현재 구현에서는 암호화 과정에서 발생할 수 있는 예외들이 상위로 전파됩니다. 더 명확한 오류 처리를 위해 커스텀 예외를 고려해보세요.
다음과 같은 예외 처리를 추가할 수 있습니다:
+ sealed class CryptoException(message: String, cause: Throwable? = null) : Exception(message, cause) { + class EncryptionException(cause: Throwable) : CryptoException("암호화 중 오류 발생", cause) + class DecryptionException(cause: Throwable) : CryptoException("복호화 중 오류 발생", cause) + } fun encrypt(plainText: String): String { + try { cipher.init(Cipher.ENCRYPT_MODE, getKey()) val iv = cipher.iv val encryptedBytes = cipher.doFinal(plainText.toByteArray()) val combined = iv + encryptedBytes return Base64.encodeToString(combined, Base64.NO_WRAP) + } catch (e: Exception) { + throw CryptoException.EncryptionException(e) + } } fun decrypt(encodedText: String): String { + try { val combined = Base64.decode(encodedText, Base64.NO_WRAP) val iv = combined.copyOfRange(0, IV_SIZE) val encrypted = combined.copyOfRange(IV_SIZE, combined.size) cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) val decryptedString = String(cipher.doFinal(encrypted)) return decryptedString + } catch (e: Exception) { + throw CryptoException.DecryptionException(e) + } }
67-67: 키 별칭을 더 구체적으로 명명하세요.현재 "secret"이라는 일반적인 키 별칭을 사용하고 있습니다. 더 구체적이고 의미 있는 이름을 사용하는 것이 좋습니다.
- private const val KEY_ALIAS = "secret" + private const val KEY_ALIAS = "booket_token_encryption_key"core/datastore/src/androidTest/kotlin/com/ninecraft/booket/core/datastore/TokenPreferenceDataSourceTest.kt (1)
68-79: 복호화 검증 테스트가 적절합니다.저장된 토큰이 정확히 복호화되는지 확인하는 테스트입니다. 엣지 케이스를 추가로 고려해보세요.
다음과 같은 추가 테스트 케이스들을 고려해보세요:
@Test fun clearTokens_removesAllStoredTokens() = runTest { // Given dataSource.setAccessToken("access_token") dataSource.setRefreshToken("refresh_token") // When dataSource.clearTokens() // Then assertEquals("", dataSource.accessToken.first()) assertEquals("", dataSource.refreshToken.first()) } @Test fun emptyTokenHandling() = runTest { // Given dataSource.setAccessToken("") // When val retrievedToken = dataSource.accessToken.first() // Then assertEquals("", retrievedToken) } @Test fun specialCharactersInToken() = runTest { // Given val specialToken = "token_with_특수문자_!@#$%^&*()" dataSource.setAccessToken(specialToken) // When val retrievedToken = dataSource.accessToken.first() // Then assertEquals(specialToken, retrievedToken) }core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/di/DataStoreModule.kt (1)
32-41: 인터페이스 바인딩이 적절합니다.구현체를 인터페이스에 바인딩하여 의존성 주입을 가능하게 했습니다.
두 개의 별도 모듈보다는 하나의 모듈로 통합하는 것을 고려해보세요:
@Module @InstallIn(SingletonComponent::class) -object DataStoreModule { +abstract class DataStoreModule { private const val TOKEN_DATASTORE_NAME = "TOKENS_PREFERENCES" private val Context.dataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME) - @Provides - @Singleton - @Named("token") - fun provideTokenDataStore( - @ApplicationContext context: Context, - ): DataStore<Preferences> = context.dataStore -} - -@Module -@InstallIn(SingletonComponent::class) -abstract class DataStoreBindModule { + companion object { + @Provides + @Singleton + @Named("token") + fun provideTokenDataStore( + @ApplicationContext context: Context, + ): DataStore<Preferences> = context.dataStore + } @Binds @Singleton abstract fun bindTokenPreferencesDataSource( tokenPreferencesDataSourceImpl: TokenPreferencesDataSourceImpl, ): TokenPreferencesDataSource }core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/TokenPreferencesDataSourceImpl.kt (1)
48-53: 로깅 시 보안 정보 노출 위험성을 점검해주세요.복호화 실패 시 예외를 로깅하고 있는데, 예외 메시지나 스택 트레이스에 민감한 정보가 포함될 가능성을 검토해주세요.
보안을 강화하려면 다음과 같이 수정을 고려해보세요:
} catch (e: GeneralSecurityException) { - Logger.e(e, "Failed to decrypt token") + Logger.e("Failed to decrypt token: ${e.javaClass.simpleName}") "" }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
core/datastore/.gitignore(1 hunks)core/datastore/build.gradle.kts(1 hunks)core/datastore/src/androidTest/kotlin/com/ninecraft/booket/core/datastore/TokenPreferenceDataSourceTest.kt(1 hunks)core/datastore/src/main/AndroidManifest.xml(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/TokenPreferencesDataSource.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/TokenPreferencesDataSourceImpl.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/di/DataStoreModule.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/security/CryptoManager.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/util/DataStoreUtil.kt(1 hunks)gradle/libs.versions.toml(5 hunks)settings.gradle.kts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: ci-build
🔇 Additional comments (23)
core/datastore/.gitignore (1)
1-1: 빌드 출력 무시 설정 확인
core/datastore 모듈에서 생성되는build디렉토리를 정상적으로 무시하고 있습니다. 추가로local.properties나 IDE별 설정 파일(.idea/) 등도 필요한 경우 무시 리스트에 포함할지를 고려해보세요.settings.gradle.kts (1)
27-29: 새 모듈 포함 선언 검증
:core:datastore모듈이include(...)에 올바르게 추가되었습니다. 멀티 모듈 빌드 설정과 일관성이 유지되고 있는지 확인했으며 문제 없습니다.gradle/libs.versions.toml (6)
57-59: 테스트 라이브러리 버전 추가 승인
AndroidX 테스트 확장, 테스트 러너, Espresso Core 버전이 적절히 분리되어 추가되었습니다. 테스트 기반이 충돌 없이 동작할 것으로 보입니다.
72-72: AndroidX DataStore 의존성 업데이트 확인
androidx-datastore항목이datastore-preferences로 정확히 명시되었으며, 모듈에서 참조하는 라이브러리와 일치합니다.
93-93: Coroutines Test 의존성 추가 승인
kotlinx-coroutines-test라이브러리가 Android 테스트에서 적절히 활용될 수 있도록 추가되었습니다.
137-137: booket-android-retrofit 플러그인 정의 확인
프로젝트 고유 플러그인booket-android-retrofit정의가 trailing space 없이 올바르게 추가되었습니다.
63-64: settings.gradle.kts에서org.jetbrains.kotlin:kotlin-gradle-plugin참조가 검색되지 않습니다. 빌드스크립트 전반에서 해당 플러그인 버전이 제대로 적용되는지 확인하기 위해 다음 명령어를 실행해 보세요.#!/bin/bash # 전체 코드베이스에서 kotlin-gradle-plugin 관련 참조 검색 grep -R "kotlin-gradle-plugin" -n . # 버전 카탈로그 alias 사용 확인 (camelCase 변환 alias) grep -R "libs\.kotlinGradlePlugin" -n . grep -R "classpath.*libs\.kotlinGradlePlugin" -n .
110-112:androidTestImplementations블록 내 의존성 검증
androidx-test-ext-junit,androidx-test-runner,androidx-espresso-corealias가 실제로 사용되고 있는지 블록 내부를 확인해주세요. 아래 스크립트를 실행해 주세요:#!/bin/bash # androidTestImplementations 블록에 어떤 라이브러리들이 등록되었는지 확인 rg -A5 "androidTestImplementations" -n core/datastore/build.gradle.ktscore/datastore/src/main/AndroidManifest.xml (1)
1-4: AndroidManifest 설정 검토
라이브러리 모듈에서namespace를 Gradle 설정으로 관리하고 있어package속성이 생략된 것은 허용됩니다. 현재 빈<manifest>구조도 문제 없습니다.core/datastore/build.gradle.kts (5)
1-1: Suppress 어노테이션 사용 검증
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")로 상위 플랫폼 인라인 경고를 억제한 것은 적절해 보입니다.
3-6: 플러그인 DSL 적용 검증
alias(libs.plugins.booket.android.library)및alias(libs.plugins.booket.android.hilt)가 버전 카탈로그의booket-android-library,booket-android-hilt와 일치합니다.
9-13: 네임스페이스 및 테스트 러너 설정 확인
namespace = "com.ninecraft.booket.core.datastore"와testInstrumentationRunner가 의도한 대로 지정되었습니다.
17-21: 주요 의존성 선언 검증
libs.androidx.datastore와libs.logger의implementations(...)호출이 적절하며, 버전 카탈로그와 일치합니다.
23-27: AndroidTest 의존성 선언 확인
androidTestImplementations(...)에 필요한 테스트 라이브러리가 모두 포함되어 있습니다.core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/util/DataStoreUtil.kt (1)
9-16: 안전한 DataStore 예외 처리 구현이 잘 되었습니다.DataStore에서 발생하는 IOException을 적절히 처리하여 빈 Preferences를 반환하는 표준적인 패턴을 구현했습니다. 다른 예외는 재throw하여 적절한 오류 전파를 보장합니다.
core/datastore/src/androidTest/kotlin/com/ninecraft/booket/core/datastore/TokenPreferenceDataSourceTest.kt (1)
52-65: 암호화 검증 테스트가 잘 구현되었습니다.토큰이 실제로 암호화되어 저장되는지 확인하는 테스트로, 보안 요구사항을 검증하는 중요한 테스트입니다.
core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/di/DataStoreModule.kt (1)
18-30: DataStore 제공자가 올바르게 구현되었습니다.@nAmed 어노테이션을 사용하여 다른 DataStore 인스턴스와 구분하고, 싱글톤 스코프를 적절히 적용했습니다.
core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/TokenPreferencesDataSourceImpl.kt (6)
16-19: 클래스 구조와 의존성 주입이 잘 설계되었습니다.Hilt를 사용한 의존성 주입과
@Named어노테이션을 통한 DataStore 구분이 적절하게 구현되어 있습니다. CryptoManager와의 조합으로 보안성도 확보되었습니다.
20-21: 반응형 토큰 접근 방식이 효율적입니다.Flow를 사용하여 토큰 변경 사항을 반응적으로 처리하고, 암호화된 데이터의 복호화를 투명하게 처리하는 구조가 좋습니다.
23-33: 토큰 저장 시 암호화 처리가 적절합니다.두 메서드 모두 토큰을 암호화한 후 저장하는 보안 요구사항을 잘 충족하고 있습니다. DataStore의
edit함수를 사용한 원자적 업데이트도 올바르게 구현되었습니다.
35-40: 토큰 삭제 로직이 안전하게 구현되었습니다.두 토큰을 모두 제거하는 원자적 연산이 적절하게 구현되어 있습니다.
42-55: 복호화 실패 처리 방식을 검토해주세요.복호화 실패 시 빈 문자열을 반환하는 것이 적절한지 검토가 필요합니다. 일부 경우에는 null이나 특별한 상태를 나타내는 것이 더 나을 수 있습니다.
다음 시나리오들을 고려해보세요:
- 앱 업데이트로 인한 암호화 키 변경 시
- 기기 보안 설정 변경으로 인한 키 무효화 시
- 데이터 손상으로 인한 복호화 실패 시
각 경우에 빈 문자열 반환이 적절한 동작인지 확인해주세요.
57-60: 상수 정의가 명확하고 일관성 있습니다.토큰 키를 companion object에서 private으로 정의한 것이 적절하며, 네이밍 컨벤션도 일관성 있게 적용되었습니다.
| import kotlin.io.path.createTempDirectory | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class TokenPreferenceDataSourceTest { |
| import kotlinx.coroutines.flow.catch | ||
| import java.io.IOException | ||
|
|
||
| fun Flow<Preferences>.handleIOException(): Flow<Preferences> = |
There was a problem hiding this comment.
👍 👍 👍
이 부분 항상 중복된 코드였는데 확장함수화 해주니까 깔끔하네
| @Named("token") private val dataStore: DataStore<Preferences>, | ||
| private val cryptoManager: CryptoManager, | ||
| ) : TokenPreferencesDataSource { | ||
| override val accessToken: Flow<String> = decryptStringFlow(ACCESS_TOKEN) |
There was a problem hiding this comment.
보통 Retrofit Interceptor 또는 Authenticator 내부에서 token을 조회하는지라, flow 타입으로 값을 꺼내오지 않고 .first() 까지 붙혀서 one shot의 형태로 받긴하는데(값이 왠만하면 바뀌지 않기도하고), 이 부분은 authenticator 구현할때 수정해도 될 것 같음!
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/DefaultTokenPreferencesDataSource.kt (2)
23-33: 토큰 설정 메서드가 올바르게 구현되어 있습니다.암호화 후 저장하는 로직과 suspend 함수 사용이 적절합니다.
선택적으로 코드 중복을 줄이기 위해 공통 헬퍼 메서드를 고려해볼 수 있습니다:
+private suspend fun setEncryptedToken(key: Preferences.Key<String>, token: String) { + dataStore.edit { prefs -> + prefs[key] = cryptoManager.encrypt(token) + } +} override suspend fun setAccessToken(accessToken: String) { - dataStore.edit { prefs -> - prefs[ACCESS_TOKEN] = cryptoManager.encrypt(accessToken) - } + setEncryptedToken(ACCESS_TOKEN, accessToken) } override suspend fun setRefreshToken(refreshToken: String) { - dataStore.edit { prefs -> - prefs[REFRESH_TOKEN] = cryptoManager.encrypt(refreshToken) - } + setEncryptedToken(REFRESH_TOKEN, refreshToken) }
42-55: 복호화 로직과 예외 처리가 잘 구현되어 있지만 개선 사항을 고려해보세요.현재 구현은 안정적이지만 다음 사항들을 검토해보시기 바랍니다:
복호화 실패 시 빈 문자열 반환: 토큰 복호화 실패는 심각한 보안 문제일 수 있습니다. 빈 문자열 대신 null 반환이나 명시적 예외 처리를 고려해보세요.
로깅 보안 고려: 암호화 관련 예외를 로깅할 때 민감한 정보가 노출되지 않도록 주의가 필요합니다.
} catch (e: GeneralSecurityException) { - Logger.e(e, "Failed to decrypt token") + Logger.e("Token decryption failed: ${e.javaClass.simpleName}") "" }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
core/datastore/build.gradle.kts(1 hunks)core/datastore/src/androidTest/kotlin/com/ninecraft/booket/core/datastore/TokenPreferenceDataSourceTest.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/DefaultTokenPreferencesDataSource.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/di/DataStoreModule.kt(1 hunks)core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/security/CryptoManager.kt(1 hunks)gradle/libs.versions.toml(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- core/datastore/build.gradle.kts
- core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/security/CryptoManager.kt
- core/datastore/src/androidTest/kotlin/com/ninecraft/booket/core/datastore/TokenPreferenceDataSourceTest.kt
- core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/di/DataStoreModule.kt
- gradle/libs.versions.toml
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: ci-build
🔇 Additional comments (5)
core/datastore/src/main/kotlin/com/ninecraft/booket/core/datastore/datasource/DefaultTokenPreferencesDataSource.kt (5)
1-14: 패키지 선언과 import문이 잘 구성되어 있습니다.필요한 모든 의존성이 적절히 import되어 있고, 패키지 네이밍 규칙도 잘 따르고 있습니다.
16-19: 의존성 주입과 클래스 설계가 우수합니다.@nAmed 어노테이션을 통한 특정 DataStore 인스턴스 주입과 CryptoManager 의존성 주입이 적절히 구현되어 있습니다.
20-21: Flow 기반 속성 설계가 훌륭합니다.반응형 프로그래밍 패턴을 적용하여 토큰 변경사항을 자동으로 감지할 수 있도록 구현되어 있습니다.
35-40: 토큰 삭제 메서드가 적절히 구현되어 있습니다.단일 원자적 연산으로 두 토큰을 모두 안전하게 제거하는 구현이 좋습니다.
57-60: 상수 정의가 적절합니다.preferences key들이 올바른 네이밍 규칙과 접근 제어자로 정의되어 있습니다.
|
수고했고~ 충돌나는 부분 해결해서 머지하면 될듯합니다~ |
# Conflicts: # gradle/libs.versions.toml # settings.gradle.kts
|
YAPP-Github/Bitnagil-Android#10 리뷰 내용이 좋아서 공유 |

🔗 관련 이슈
📙 작업 설명
🧪 테스트 내역 (선택)
💬 추가 설명 or 리뷰 포인트 (선택)
Summary by CodeRabbit
신규 기능
테스트
환경설정 및 의존성